/**
 * Created with IntelliJ IDEA.
 * User: ajhin
 * Date: 9/4/13
 * Time: 11:22 PM
 * To change this template use File | Settings | File Templates.
 */
package com.societyGames.codeAlong.state.codeExercise.model.testCase
{
import com.pblabs.engine.serialization.Serializer;
import com.pblabs.engine.serialization.TypeUtility;
import com.societyGames.codeAlong.codeExercise.Executor;
import com.societyGames.codeAlong.state.codeExercise.model.*;
import com.societyGames.codeExercise.ErrorEvent;
import com.societyGames.codeExercise.ICodeExercise;
import com.societyGames.flashForms.DirectContainer;
import com.societyGames.system.EventObject;
import com.societyGames.system.FileCache;

import flash.display.DisplayObject;
import flash.display.Loader;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.TextEvent;
import flash.system.ApplicationDomain;
import flash.system.LoaderContext;
import flash.utils.ByteArray;

public class TestCase extends EventDispatcher
{
  private var _fileCache:FileCache;
  private var _caseData:CaseData;
  private var _index:int;
  private var _testCaseStatus:TestCaseStatus;

  private var _codeExerciseContainer:DirectContainer = new DirectContainer();
  private var _executor:Executor = new Executor();		//Runs the code.

  private var _codeExercise:ICodeExercise;				//Reference to the codeExercise, which is really the case.
  private var _codeExerciseLoader:Loader;						//Loader for the case.
  private var _codeExerciseApplicationDomain:ApplicationDomain;	//The application domain of the case.

  private var _codeExerciseByteArray:ByteArray; //The loaded byte array for the test case/code exercise.

  private var _executeByteArray:ByteArray; //The swf to test against. If null, we're not testing against anything, just browsing test cases.
  private var _error:Error; //The last error we received.

  private var _outputEventObject:EventObject;
  private var _testCaseStatusChangedEventObject:EventObject;

  public function TestCase(fileCache:FileCache, index:int, caseData:CaseData)
  {
    _outputEventObject = new EventObject(this, "OUTPUT", TextEvent);
    _testCaseStatusChangedEventObject = new EventObject(this, "TEST_CASE_STATUS_CHANGED", Event);

    _fileCache = fileCache;
    _index = index;
    _caseData = caseData;

    this._executor.completeEventObject.addEventListener(executorCompleteHandler, false, 0, true);

    goToState(TestCaseStatus.loadingCodeExercise);
  }

  private function goToState(newTestCaseStatus:TestCaseStatus):void
  {
    //Exit old state.
    switch (_testCaseStatus)
    {
      case null:
        //Don't do anything.
        break;
      case TestCaseStatus.loadingCodeExercise:
        exitLoadingCodeExercise();
        break;
      case TestCaseStatus.resetting:
        //Don't do anything
        break;
      case TestCaseStatus.ready:
        break;
      case TestCaseStatus.executing:
        exitExecuting();
        break;
      case TestCaseStatus.stopped:
        destroyCodeExercise();
        break;
      case TestCaseStatus.exceptioned:
        destroyCodeExercise();
        exitExceptioned();
        break;
      case TestCaseStatus.unhandledExceptioned:
        destroyCodeExercise();
        break;
      case TestCaseStatus.failed:
        destroyCodeExercise();
        break;
      case TestCaseStatus.succeeded:
        destroyCodeExercise();
        break;
      default:
        throw new Error("Unhandled exit of old state TestCaseStatus." + _testCaseStatus.name);
        break;
    }

    switch (newTestCaseStatus)
    {
      case null:
        //Don't do anything.
        break;
      case TestCaseStatus.loadingCodeExercise:
        enterLoadingCodeExercise();
        break;
      case TestCaseStatus.resetting:
        enterResetting();
        break;
      case TestCaseStatus.ready:
        //Don't do anything.
        break;
      case TestCaseStatus.executing:
        enterExecuting();
        break;
      case TestCaseStatus.stopped:
        //Don't do anything.
        break;
      case TestCaseStatus.exceptioned:
        enterExceptioned();
        break;
      case TestCaseStatus.unhandledExceptioned:
        //Don't do anything
        break;
      case TestCaseStatus.failed:
        //Don't do anything
        break;
      case TestCaseStatus.succeeded:
        //Don't do anything
        break;
      default:
        throw new Error("Unhandled enter of TestCaseStatus." + _testCaseStatus.name);
        break;
    }

    _testCaseStatus = newTestCaseStatus;
    _testCaseStatusChangedEventObject.dispatchEvent();
  }

  private function enterLoadingCodeExercise():void
  {
    _fileCache.load(_caseData.swfFileName, codeExerciseBytesLoadedHandler, codeExerciseBytesLoadFailedHandler);
  }

  private function codeExerciseBytesLoadedHandler(url:String, byteArray:ByteArray):void
  {
    _codeExerciseByteArray = byteArray;
    goToState(TestCaseStatus.resetting);
  }

  private function codeExerciseBytesLoadFailedHandler(url:String):void
  {
    trace("Failed to load: " + url);
  }

  private function exitLoadingCodeExercise():void
  {
    var isCodeExerciseLoaded:Boolean = _codeExerciseByteArray != null;
    if (!isCodeExerciseLoaded)
    {
      throw new Error("Exited TestCaseStatus.loadingCodeExercise before exercise loaded.");
    }
  }

  public function get canReset():Boolean
  {
    return _testCaseStatus == TestCaseStatus.stopped ||
            _testCaseStatus == TestCaseStatus.exceptioned ||
            _testCaseStatus == TestCaseStatus.unhandledExceptioned ||
            _testCaseStatus == TestCaseStatus.failed ||
            _testCaseStatus == TestCaseStatus.succeeded;
  }

  public function reset():void
  {
    if (!canReset)
    {
      throw new Error("Attempted to reset() when not allowed.");
    }
    goToState(TestCaseStatus.resetting);
  }

  private function enterResetting():void
  {
    createCodeExercise();
  }

  private function createCodeExercise():void
  {
    //Create our own application domain for the case code.
    _codeExerciseApplicationDomain = new ApplicationDomain(ApplicationDomain.currentDomain);
    var caseLoaderContext:LoaderContext = new LoaderContext(false, _codeExerciseApplicationDomain);
    caseLoaderContext.allowCodeImport = true; //For Editor.
    this._codeExerciseLoader = new Loader();
    //Listen for everything we need
    //_codeExerciseLoader.uncaughtErrorEvents.addEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, exerciseLoaderUncaughtErrorHandler);
    _codeExerciseLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, codeExerciseLoadCompleteHandler, false, 0, true);
    //Do the actual load of the case.
    _codeExerciseLoader.loadBytes(_codeExerciseByteArray, caseLoaderContext);
  }

  private function codeExerciseLoadCompleteHandler(event:Event):void
  {
    _codeExerciseLoader.contentLoaderInfo.removeEventListener(Event.COMPLETE, codeExerciseLoadCompleteHandler);

    //Grab a handle to the code exercise.
    var caseClass:Class = Class(_codeExerciseApplicationDomain.getDefinition(this._caseData.className));
    this._codeExercise = ICodeExercise(new caseClass());
    //Set the application domain so we can find classes specific to the exercise.
    TypeUtility.setApplicationDomain(_codeExerciseApplicationDomain);
    //The non-deserialize extra data data.
    this._codeExercise.maximumActionCount = this._caseData.maximumActionCount;
    //Deserialize the initialization XML.
    Serializer.instance.deserialize(this._codeExercise, this._caseData.initializationXml);
    //Initialize the code exercise.
    this._codeExercise.initialize();
    //Render CodeExercise
    this._codeExerciseContainer.item = this._codeExercise.displayObject;

    goToState(TestCaseStatus.ready);
  }

  private function destroyCodeExercise():void
  {
    //Get ride of all references to anything in the application domain
    _codeExerciseApplicationDomain = null;
    _codeExercise = null;
    TypeUtility.setApplicationDomain(null);
    _codeExerciseContainer.item = null;

    //Kill the old loader.
    if (this._codeExerciseLoader != null)
    {
      //_codeExerciseLoader.uncaughtErrorEvents.removeEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, exerciseLoaderUncaughtErrorHandler);
      this._codeExerciseLoader.contentLoaderInfo.removeEventListener(Event.COMPLETE, codeExerciseLoadCompleteHandler);
      this._codeExerciseLoader.unloadAndStop(true);
      this._codeExerciseLoader = null;
    }
  }

  public function get canExecute():Boolean
  {
    return _testCaseStatus == TestCaseStatus.ready || canReset;
  }

  public function execute(executeByteArray:ByteArray):void
  {
    if (!canExecute)
    {
      throw new Error("Attempted to execute() when not allowed.");
    }

    _executeByteArray = executeByteArray;
    goToState(TestCaseStatus.executing);
  }

  private function enterExecuting():void
  {
    this._codeExercise.successEventObject.addEventListener(exerciseSuccessHandler, false, 0, true);
    this._codeExercise.failureEventObject.addEventListener(exerciseFailureHandler, false, 0, true);
    this._codeExercise.errorEventObject.addEventListener(exerciseErrorHandler, false, 0, true);
    this._codeExercise.unhandledErrorEventObject.addEventListener(exerciseUnhandledErrorHandler, false, 0, true);
    this._codeExercise.outputEventObject.addEventListener(exerciseOutputHandler, false, 0, true);

    this._executor.execute(this._executeByteArray, this._codeExerciseApplicationDomain);
  }

  private function exitExecuting():void
  {
    this._codeExercise.successEventObject.removeEventListener(exerciseSuccessHandler);
    this._codeExercise.failureEventObject.removeEventListener(exerciseFailureHandler);
    this._codeExercise.errorEventObject.removeEventListener(exerciseErrorHandler);
    this._codeExercise.unhandledErrorEventObject.removeEventListener(exerciseUnhandledErrorHandler);
    this._codeExercise.outputEventObject.removeEventListener(exerciseOutputHandler);
    this._codeExercise.stop();
    _executeByteArray = null;
  }

  /*private function exerciseLoaderUncaughtErrorHandler(event:UncaughtErrorEvent):void
   {
   trace(event.error);
   reportException(event.error);
   }*/

  private function executorCompleteHandler(event:Event):void
  {
    this._codeExercise.handleUserCodeEnd(); //The user code ended. We don't know why necessarily.
  }

  private function exerciseSuccessHandler(event:Event):void
  {
    goToState(TestCaseStatus.succeeded);
  }

  private function exerciseFailureHandler(event:Event):void
  {
    goToState(TestCaseStatus.failed);
  }

  private function exerciseErrorHandler(event:ErrorEvent):void
  {
    _error = event.error;
    goToState(TestCaseStatus.exceptioned);
  }

  private function exerciseUnhandledErrorHandler(event:Event):void
  {
    goToState(TestCaseStatus.unhandledExceptioned);
  }

  private function exerciseOutputHandler(event:TextEvent):void
  {
    this._outputEventObject.dispatchEvent(event.clone());
  }

  public function get canException():Boolean
  {
    return _testCaseStatus == TestCaseStatus.executing;
  }

  public function exception(error:Error):void
  {
    if (!canException)
    {
      throw new Error("Attempted to exception() when not allowed.");
    }
    _error = error;
    goToState(TestCaseStatus.exceptioned);
  }

  public function get canStop():Boolean
  {
    return _testCaseStatus == TestCaseStatus.executing;
  }

  public function stop():void
  {
    if (!canStop)
    {
      throw new Error("Attempted to stop() when not allowed.");
    }

    goToState(TestCaseStatus.stopped);
  }

  private function enterStopped():void
  {
    this._codeExercise.stop();
  }

  private function enterExceptioned():void
  {
    if (_error == null)
    {
      throw new Error("Entered state TestCaseStats.Exceptioned without _error set.");
    }
  }

  private function exitExceptioned():void
  {
    _error = null;
  }

  public function get error():Error
  {
    return _error;
  }

  public function get index():int
  {
    return _index;
  }

  public function get displayObject():DisplayObject
  {
    return this._codeExerciseContainer;
  }

  //TextEvent
  public function get outputEventObject():EventObject
  {
    return _outputEventObject;
  }

  //Event
  public function get testCaseStatusChangedEventObject():EventObject
  {
    return _testCaseStatusChangedEventObject;
  }

  public function get testCaseStatus():TestCaseStatus
  {
    return this._testCaseStatus;
  }
}
}
